閱讀本篇文章前,仔細想想看
- 當函式遇到 100% 無法跳脫或會拋出例外的狀況,這時 TypeScript 會如何對該函式進行推論?
never
型別為所有型別的 Subtype —— 請問這使得never
擁有什麼特性?never
和別的型別進行複合(union
與intersection
)會發生什麼事?- 試問 TypeScript 裡最需要避免的狀況有哪些?(答案如果能夠列舉越多,代表讀者越清楚 TypeScript 的雷點)
- 試問 TypeScript 裡最需要主動對變數(或函式的參數)作型別註記的時機?(同上,答案如果能夠列舉越多,代表讀者越清楚 TypeScript 的型別推論機制)
- 承上題,我們允許讓 TypeScript 對變數作自由地型別推論的時機又有哪些?
前三題都是前一篇文章所解答過後的知識,而後面三題則是考驗目前讀者對於 TypeScript 的型別推論與註記理解到什麼程度。因此呢~後面三題能夠完美地回答出來(不是怪物、天才不然就是經驗豐富的高手),就表示讀者確實把 Day 02 看到 Day 09 的內容全部看完而且有內化進去!
要說筆者能不能 100% 完美解答?其實對筆者來說也很困難,一下子要在短時間內記那麼多 Case 是很難的(所以都是靠經驗久了自然就會注意到)。另外,最好的學習方式除了實作之外,寫文章教別人也是很棒的方式。
[2019.09.15 新增] tsconfig.json 設定
這裡筆者必須緊急說明:若讀者試著筆者舉的程式碼範例的話,請讀者記得將裡面的
strictNullCheck
選項改成true
,這一點忘記在文章系列的一開頭提醒讀者,實在是很抱歉!/* tsconfig.json */ { "compilerOptions": { /* ... */ "strictNullChecks": true, /* ... */ } }
因此請讀者注意,目前學習的 TypeScript 型別系統版本多了一個
strictNullCheck
的編譯器屬性設定!至於為何會造成如此狀況,那是因為筆者在專案上習慣將某些 TypeScript 編譯器設定啟動!至於strictNullCheck
到底為何,將會在型別系統講述告一段落後,開始講述 TypeScript 的編譯器設定檔喔!
看來今天是《前線維護》篇章系列,看起來比較有 BOSS 級別的感覺啊。讀者準備好進行討伐後,那麼我們就...
正文開始!
any
& unknown
型別這邊讀者還是得注意一點,未來 TypeScript 說不定還會再出更多型別(但筆者也想不到會是什麼)。但就目前來說,TypeScript 第三版後出現的
unknown
型別,已經算是最新的 Feature,因此這裡將unknown
視為本篇章的最後一道關卡。
any
型別看到這裡,筆者必須提醒 —— 真的非不得已狀態下或者是快速測試下,可以使用 any
。不過開發上,儘量不要用到 any
比較好;或者真不巧,遇到 any
,也應當主動註記。
從本系列文章的開頭就講了:
any
會造成 TypeScript 跳過檢測使得變數容易引發非預期行為的機率完全上升了。
沒什麼特殊的重點,但就是得列出我們從這系列文章得知,any
可能會在哪些情況出現:
重點 1.
any
出現的時機
- 遲滯性指派 Delayed Iniitialization:變數定義時,除了未加註記(Type Annotation)外,也沒有指派值或者被指派為 Nullable Types。(參照 Day 02.)
- 一般宣告下的函式參數:一般被宣告的函式,其參數通常會直接被推論為
any
,又被稱作 Implicitany
的情形。此狀況是少數會被 TypeScript 主動通報的(參照 Day 04.)- 函式回傳之值:有些實務上,型別無法確定,因此到最後只能將回傳值預設為
any
(如:JSON.parse
)(參照Day 04.)- 未註記之空陣列:沒有積極型別註記到的空陣列,其預設推論為
any[]
(參照 Day 05.)- 跟 I/O 行為有關:例如,從外部 CSV 檔案讀取表格行格式(通常用陣列或元組型別),若沒有特殊註記的話,通常會用
any
作表示(這可能是讀者少數會主動用any
的狀況)(參照 Day 06.)以及 ...
- 其他筆者沒有想到的狀況 XD(重點的大部分範圍都涵蓋在前五點喔)
看到這裡,讀者應該可以確保自己不誤入到 TypeScript 的 any
下的陷阱吧!當然,接下來又是挺整人的時刻~
unknown
型別的機制探討不過研究過程中,筆者意外發現,unknown
的機制挺不錯!就讓筆者給大家看看官方 TypeScript 3.0 Unknown Type 是怎麼說的:
TypeScript 3.0 introduces a new top type
unknown
.unknown
is the type-safe counterpart ofany
. Anything is assignable tounknown
, butunknown
isn’t assignable to anything but itself andany
without a type assertion or a control flow based narrowing. Likewise, no operations are permitted on anunknown
without first asserting or narrowing to a more specific type.
這裡為了讓讀者好理解,我們就拆成兩段。
unknown
相對 any
來說,是一種更安全的型別機制(a type-safe counterpart)
unknown
is the type-safe counterpart ofany
. Anything is assignable tounknown
, butunknown
isn’t assignable to anything but itself andany
without a type assertion or a control flow based narrowing.
第一句話我們就略過,因為被筆者標註在上面 XD —— unknown
是更安全的型別機制。
今天的目標就是要理解:為何 unknown
相對 any
來說,型別上的使用更加安全?
第一段中的第二句話超長,我們先來看前半部分:
“Anything is assignable to
unknown
”
unknown
與 any
的共通點是:只要當變數被註記為 any
或 unknown
,該變數照樣都可以接收任意型別的值。(檢測結果如圖一)
圖一:any
跟 unknown
之間註記在變數上,被指派任意值都沒差
好的,這裡應該沒問題,不過可能讀者會想說:“不是都說 unknown
比 any
安全嗎!?安全兩字在哪!?”(拍桌)
關鍵點在後半段這句:
“but
unknown
isn’t assignable to anything but itself andany
without a type assertion or a control flow based narrowing.”
我們來試試看下面這個例子。(使用 any
指派到任何型別的變數之檢測結果如圖二,而使用 unknown
型別的變數指派則是如圖三)
圖二:any
型別跟我們預期的一樣,就是一個廢廢的型別
圖三:哦?unknown
型別的值不能被強行指派到 —— 除了 any
或 unknown
型別外 —— 的任意型別變數
讀者應該可以看出一些 unknown
型別的好處,筆者也搶先補充一個適合使用 unknown
的情境:
如果根據
any
可能出現的時機(本篇文章重點 1.)之第 5 點,也就是無法預測的 I/O 行為。開發者可以開發比較不安全版本的讀取 CSV 檔案的函式,也就是回傳any
型別的格式。然而,開發者也可以開發安全版本的讀取 CSV 檔案的函式,其回傳的型別為
unknown
—— 代表只要任何開發者使用這個安全版本的函式回傳之值,使用者必須強行註記該 CSV 回傳值之格式。就算開發者忘記要註記結果,也會被 TypeScript 主動警告。另一個直截了當可以馬上使用
unknown
的時機,就是寫一個安全的函式(或方法)把不安全的函式(或方法)包裝起來。比如說,把JSON.parse
這種會回傳any
的方法函式包裝起來,變成:後面會再以這個例子讓大家知道
unknown
的好處,請繼續看下去!
不過在我們進到下一部分前,筆者還沒講完這部分,因為還有一些東西還沒講完:
“but
unknown
isn’t assignable to anything but itself andany
without a type assertion or a control flow based narrowing.”
把講過的部分去除並簡化成:
“but
unknown
isn’t assignable to anything but control flow based narrowing.”
讀者看到這一段知道在講什麼嗎?
“control flow based narrowing”
其實它的概念是 —— 只要程式根據判斷式與敘述式的結構,縮小變數在型別推論上的範疇,我們就可以讓純 unknown
型別的變數被指派到任意型別上。不解釋直接先看下方的例子(檢測結果如圖四):
圖四:結果我們藉由所謂的型別系統的一種技巧 -- Type Guard 限制型別被推論到的可能性 —— 來 Bypass unknown
型別原先的限制 —— 不能被指派到被註記到的任意型別(除了 unknown
與 any
)的變數
貼心小提示
這是讀者第一次在本系列看到所謂的型別限縮的技巧,又被稱為 Type Guard,但個人覺得適合的翻譯應該是『 型別檢測 』,不過筆者還是會稱它為型別限縮的技巧。
本技巧的一些細節將會跟複合型別(
union
與intersection
)一起講到(Day 17.)。簡單知道過後就讓筆者繼續回歸本日的主題。
以上的例子,一開始直接把 isUnknown
指派到 number
型別的 isNumber
變數裡,理所當然會出現紅色的警告線,被 TypeScript 警告。
但是藉由簡單的 if...else...
敘述式,將 isUnknown
的型別推論限縮到 number
型別 -- 因此TypeScript 可以根據這樣的結構斷定在這控制結構裡面的 isUnknown
必為 number
型別!這就是為何 isUnknown
在型別推論的限縮下仍然可以安全地被指派到 isNumber
變數裡(被指派到其他型別的變數)。
還有另一種方式可以將 unknown
型別的值指派到一般註記的變數裡,就是用顯性的型別註記。(Explicit Type Annotation)但是讀者早就看過了這種做法。
不過這裡的運用就是開發者必須很確定自己到底在做什麼,才會跟 TypeScript 講說,這個 unknown
型別的變數實質上是某某型別(檢測結果如圖五):
圖五:強制將型別註記限制在 unknown
型別上
哇,簡簡單單的 unknown
型別就這樣被轉型了!要是人生能夠自由快速地轉型就真的好輕鬆啊~
重點 2.
unknown
型別下的變數指派限制
- 跟
any
型別相似的地方在於:若變數被unknown
型別註記,則該變數可以被任意型別的值指派- 若被註記為
unknown
型別的變數,除了以下情形外,否則不得將其值指派到任意型別撇除unknown
或any
型別的變數裡:
- 顯性註記之型別
T
等同於被指派到的變數之型別T
- 根據程式的控制流程分析,其
unknown
型別的推論被限縮到特定的型別U
致使可以被指派到其他型別U
的變數
unknown
型別則不能輕舉妄動這個副標題其實就已經把第二句話,也就是以下這句話講得清清楚楚的:
“no operations are permitted on an
unknown
without first asserting or narrowing to a more specific type.”
第二段在探討的不是 unknown
型態的指派行為(那是剛剛第一段在探討的)。讀者可以這麼想:
unknown
型態的變數,基本上被鎖住不能使用了!
讀者看到想說:“搞毛啊!不就變唯讀的感覺嗎?”。
要讀取該變數確實是可以讀的(用 console.log
並且編譯過後執行 XD)。除此之外,被註記為 unknown
的變數什麼事情的不能做!除非開發者對該變數進行顯性的型別註記亦或者根據程式碼的控制流程分析判斷說該變數的型別被限縮到某個範疇,該變數才有機會做一些事情。(比如:呼叫屬性、方法等)
等等,這跟第一段講述的差異在哪?不也是顯性註記過後,亦或者控制流程分析過後限縮型別 -- 以上兩種情形嗎?
注意第一段跟第二段探討的重點差異:
第一段:
unknown
is the type-safe counterpart ofany
. Anything is assignable tounknown
, butunknown
isn’t assignable to anything but itself andany
without a type assertion or a control flow based narrowing.第一段在描述
unknown
型別變數指派的機制(Assignment)
比較:
第二段:“no operations are permitted on an
unknown
without first asserting or narrowing to a more specific type.”第二段針對
unknown
型別可以做的事情:重點在探討unknown
型別的『行動範圍』(Operation)
也就是說,unknown
型別的變數基本上一丁點事情都沒辦法做,除非我們刻意跟 TypeScript 講說它是哪一種型別,亦或者是根據控制流程分析限縮型別的範疇讓 TypeScript 推斷說該變數確切的型別是什麼(運用上一部分講述的型別限縮的技巧 -- Type Guard)。
以下舉例(推論結果為圖六):
圖六:結果 any
型別 TypeScript 不會去理會,但 unknown
的行動完全被鎖住
你可以看到以上的結果:any
型別不管亂呼叫什麼東西,都不會有事。
然而 unknown
型別就不同啦~ 只要亂動 unknown
型別,TypeScript 就會跟你說:“該變數存的物件是 unknown
,因此你不能對它做任何事情或呼叫任何方法”。(錯誤訊息如圖七)
圖七:所以 TypeScript 會幫助我們鎖定 unknown
型別的行動
要使得 unknown
型別是可以被啟動的,我們可以試試看下面的範例。(顯性型別註記為圖八,而型別限縮為圖九)
圖八:針對 unknown
型別作型別註記即可立即使用
圖九:編譯器自行判斷該 unknown
型別確定被推論到某種型別後就即可使用
重點 3.
unknown
型別變數的特性假設某變數
A
被指派為unknown
型別,則:
A
不能呼叫任何方法或屬性,亦不可作為任何函式或方法之參數A
不能指派到型別為T
的變數,其中T
不為unknown
或any
型別 (根據本篇重點 2.)- 若
A
被顯性地型別註記為某型別T
(其中T
不為unknown
),則A
可以作為該型別T
之代表值,進行該型別底下合理之操作- 若
A
被控制流程限縮型別至某型別T
(其中T
不為unknown
),則A
可以作為該型別T
之代表值,在該控制流程的範圍內進行合理之操作
在討論第一段的後面,筆者提到可以寫一個安全的函式(或方法)把不安全的函式(或方法)包裝起來。比如說,把 JSON.parse
這種會回傳 any
的方法函式包裝起來。這樣的好處就由以下程式碼來進行驗證吧!(驗證結果為圖十,錯誤訊息為圖十一)
圖十:直接使用 JSON.parse
會回傳 any
;然而,使用安全的 unknown
回傳型別的函式會自動讓變數註記成 unknown
,每次回傳的值必須被強行註記才可使用
圖十一:TypeScript 確保我們不會隨隨便便亂用變數的屬性,實在是很聰明的~
從剛剛的範例可以知道,筆者將 JSON.parse
用 safelyParseJSON
這個函式包裝後,只要將不明的 JSON 物件解析出來,就一定要遵照使用 unknown
變數的原則:必須顯性地註記才能使用。因此 unknown
可以協助我們解決本篇重點 1 談到的 any
的出現狀況的第 3 點和第 5 點的案例呢!
unknown
型別的複合特性哇!我們還沒討論完啊?
快完成囉~這邊討論的概念跟 never
很像,但是又比 never
稍嫌麻煩一些。
根據已知 unknown
的特性:只要我們對 unknown
型別的變數作顯性型別註記,該型別就會取代 unknown
的狀態!
運用複合型別的概念反過來推論:任何型別只要和 unknown
交集(intersection
)在一起,unknown
就會被任意型別吸收。
例如:如果 number
型別跟 unknown
型別 intersect 在一起,該變數要同時為 number
也同時為 unknown
型別的話,那就只會有一種狀況 —— 該變數本身就是 number
型別。
因此,以下的程式碼推論出來的結果,都會將 unknown
型別給吸收掉。(圖十二為官方的範例程式碼截圖,其中,所有的 unknown
都被其他型別藉由 intersection
被吸收掉了)
圖十二:unknown
跟任何型別進行 intersection
時,就會被該型別吸收掉
任何型別如果可以同時是 —— 比如 number
型別或 unknown
型別,那這樣的狀況是不是變得不確定了?『 同時 』的概念跟 union
很像。因此 unknown
跟 number
進行 union
時,反而是 unknown
吸收掉 union
型別。
但是呢,如果 unknown
型別跟 any
型別進行 union
可就不同囉,要可以同時是 unknown
或者是 any
—— 由於 any
的自由程度大過於 unknown
,因此這裡的邏輯推斷反而是 any
會把 unknown
型別吸收掉。(圖十三為官方的範例程式碼截圖,所有的 unknown
吸收掉所有跟它 union
的型別,除了 any
)
圖十三:unknown
跟任何型別(除了 any
) 進行 union
後,會吸收掉該型別
重點 4.
unknown
型別進行複合的作用
- 任何與
unknown
型別進行intersection
過後的型別T
,則T
會吸收掉unknown
:type U = unknown & T; // => T
- 任何與
unknown
型別進行union
過後的型別T
,且T
不為any
型別,則unknown
會吸收掉T
:type U = unknown | T; // => unknown type V = unknown | any; // => any
筆者寫到這裡,也把整個型別系統大部分都講完了,包含:
相信讀者讀到這裡也了解了大部分的 TypeScript 型別系統的機制。不過筆者承認,這應該是本系列枯燥乏味的部分。然而,不理解這裡面細微的機制與作用(還有潛在的雷)會造成開發過程很痛苦啊。因此筆者也不敢草草帶過這個部分,才儘量寫得不要太像在讀 Document 而是有推斷過後、循序漸進的感覺。
而昨天和今天所探討的 never
以及 unknown
這兩個特殊型別的機制之所以要從官方文件著手的理由 —— 它不算是可以靠邏輯推論得知的行為,而是 TypeScript 本身的 Feature 啊!既然很難推論的話,這時候才是讀 Doc 好時機。